package org.vectrics.location;

import java.util.Collection;
import java.util.Iterator;
import java.util.List;
import java.util.TreeSet;

import org.apache.log4j.Logger;
import org.hibernate.Session;
import org.vectrics.DataException;
import org.vectrics.SystemException;
import org.vectrics.common.db.Criteria;
import org.vectrics.common.util.StringUtils;
import org.vectrics.domain.Domain;
import org.vectrics.hibernate.Filter;
import org.vectrics.hibernate.HibernateUtil;
import org.vectrics.hibernate.Selector;
import org.vectrics.hibernate.ThreadProperties;
import org.vectrics.provider.CoreServiceLocator;
import org.vectrics.user.Person;

/**
 * Implementation of LocationService interface that utilizes Hibernate for
 * persistence.
 * 
 * @author Mike Moore
 * 
 */
public class LocationPersistMgr implements LocationService {

    private transient static Logger log = Logger.getLogger(LocationPersistMgr.class);

    public LocationPersistMgr() {
    }

    /**
     * Remove the domain location.
     */
    public void removeDomainLocation(final Domain domain, final String contextCode, final Location location) {
        DomainLocation domainLocation = findDomainLocation(domain, location, contextCode);
        if (domainLocation != null) {
            ThreadProperties.getSession().delete(domainLocation);
        }
    }

    /**
     * Create domain location with passed context.
     */
    public DomainLocation createDomainLocation(final Domain domain, final String contextCode, final Location location) {
        DomainLocation domainLocation = new DomainLocation();
        domainLocation.setDomain(domain);
        domainLocation.setLocation(location);
        domainLocation.setTypeCode(contextCode);
        ThreadProperties.getSession().save(domainLocation);
        return domainLocation;
    }

    /*
     * Create location and save to database
     * 
     * @see
     * org.vectrics.location.LocationService#createLocation(org.vectrics.domain
     * .Domain, org.vectrics.location.Location, java.lang.String,
     * java.lang.String, java.lang.String)
     */
    public Location createLocation(final Domain domain, final Location parent, final String name,
            final String description, final String typeCode, final String abbreviation) {
        Location newLocation = new Location();
        Session session = ThreadProperties.getSession();
        newLocation.setName(name);
        newLocation.setDomain(domain);
        newLocation.setParent(parent);
        newLocation.setDescription(description);
        newLocation.setAbbreviation(abbreviation);
        newLocation.setTypeCode(typeCode);
        if (newLocation.getParent() == null) {
            newLocation.setApproved(true);
        }
        session.save(newLocation);
        return newLocation;
    }

    public Long getNextLocationId() {
        Long id = CoreServiceLocator.getSurrogateKeyService().getKey("location.id");
        return id;
    }

    private void copyChildrenLocations(final Location fromParent, final Location toParent) {
        Iterator iter = fromParent.getChildren().iterator();
        if (log.isDebugEnabled()) {
            log.info("Copying children of parent location." + "\r\n - Parent: " + fromParent.getName()
                    + "\r\n Child count: " + fromParent.getChildren().size());
        }
        while (iter.hasNext()) {
            Location fromLocation = (Location) iter.next();
            if (log.isDebugEnabled()) {
                log.debug("Copying child location: " + fromLocation.getName());
            }
            Location newLocation = toParent.addChild();
            newLocation.setId(getNextLocationId());
            newLocation.setTypeCode(fromLocation.getTypeCode());
            newLocation.setName(fromLocation.getName());
            ThreadProperties.getSession().save(newLocation);
            copyChildrenLocations(fromLocation, newLocation);
        }
    }

    /**
     * Copy locations from one domain to another.
     */
    public void copyLocations(final Domain fromDomain, final Domain toDomain) {
        Session session = ThreadProperties.getSession();
        Filter filter = new Filter();
        filter.addFrom("org.vectrics.location.DomainLocation", "doLocation");
        filter.addSelectField("doLocation");
        filter.addParameterCondition("doLocation.domain.id", "=", fromDomain.getId());
        List all = filter.createQuery(session).list();
        Iterator iter = all.iterator();
        while (iter.hasNext()) {
            DomainLocation doLocation = (DomainLocation) iter.next();
            DomainLocation newDoLocation = new DomainLocation();
            newDoLocation.setDomain(toDomain);
            newDoLocation.setTypeCode(doLocation.getTypeCode());
            // Location location = this.findLocation(toDomain,
            // doLocation.getLocation().getName(),
            // doLocation.getLocation().getTypeCode());
            newDoLocation.setLocation(doLocation.getLocation());
            session.save(newDoLocation);
        }
    }

    /**
     * Find locations with domain, name and type. In addition search parent
     * domains for the same name and type.
     */
    public Collection findLocation(final Domain domain, final String name, final String typeCode) {
        Session session = ThreadProperties.getSession();
        Filter filter = new Filter();
        filter.addFrom("org.vectrics.location.Location", "location");
        filter.addSelectField("distinct(location)");

        Selector domainSelector = filter.getSelector().addChildSelector(Selector.CONJUNTION_OR);
        Domain currentDomain = domain;
        while (currentDomain != null) {
            domainSelector.addParameterCondition("location.domain.id", "=", currentDomain.getId());
            currentDomain = currentDomain.getParent();
        }

        filter.addParameterCondition("location.name", "=", name);
        filter.addParameterCondition("location.typeCode", "=", typeCode);

        List all = filter.createQuery(session).list();
        /*
         * if (all.size() > 1) { String listOfLocations = ""; Iterator iter =
         * all.iterator(); while (iter.hasNext()) { Location location =
         * (Location)iter.next(); listOfLocations += location.getId() + " -|- ";
         * }
         * 
         * throw new
         * SystemException("Found more than one location with criteria:" +
         * "\r\n - Domain:        " + domain.getId() + "\r\n - Name:          "
         * + name + "\r\n - Location ids:  " + listOfLocations +
         * "\r\n - typeCode:      " + typeCode ); } Location location = null;
         * 
         * if (all.size() < 1) { location = null;
         * log.info("Could not find location- domain:" + domain + " Name:" +
         * name + "  TypeCode: " + typeCode); } else { location =
         * (Location)all.iterator().next(); }
         */
        return all;
    }

    /**
     * Find locations with domain, name and type. In addition search parent
     * domains for the same name and type.
     */
    public Location findLocation(final Location parentLocation, final Domain domain, final String name,
            final String typeCode) {
        Session session = ThreadProperties.getSession();
        Filter filter = new Filter();
        filter.addFrom("org.vectrics.location.Location", "location");
        filter.addSelectField("distinct(location)");

        Selector domainSelector = filter.getSelector().addChildSelector(Selector.CONJUNTION_OR);
        Domain currentDomain = domain;
        while (currentDomain != null) {
            domainSelector.addParameterCondition("location.domain.id", "=", currentDomain.getId());
            currentDomain = currentDomain.getParent();
        }

        filter.addParameterCondition("location.name", "=", name);
        filter.addParameterCondition("location.typeCode", "=", typeCode);
        if (parentLocation != null) {
            filter.addParameterCondition("location.parent.id", "=", parentLocation.getId());
        } else {
            filter.addCondition("location.parent.id is null");
        }

        List all = filter.createQuery(session).list();
        if (all.size() > 1) {
            String listOfLocations = "";
            Iterator iter = all.iterator();
            while (iter.hasNext()) {
                Location location = (Location) iter.next();
                listOfLocations += location.getId() + " -|- ";
            }

            throw new SystemException("Found more than one location with criteria:" + "\r\n - Domain:        "
                    + domain.getId() + "\r\n - Name:          " + name + "\r\n - Location ids:  " + listOfLocations
                    + "\r\n - typeCode:      " + typeCode);
        }
        Location location = null;

        if (all.size() < 1) {
            location = null;
            log.info("Could not find location- domain:" + domain + " Name:" + name + "  TypeCode: " + typeCode);
        } else {
            location = (Location) all.iterator().next();
        }

        return location;
    }

    /*
     * Find the Location object for the passed id.
     */
    public Location findLocation(final Long id) {
        if (log.isDebugEnabled()) {
            log.debug("findLocation() - id = " + id);
        }
        Location location = null;
        Session session = ThreadProperties.getSession();
        location = (Location) session.load(Location.class, id);
        log.debug("findLocation() - location = " + location.getName());
        if (location == null)
            throw new SystemException("Could not find location: " + id);
        return location;
    }

    public void updateLocation(final Location location) {
        if (location.getDomain() == null)
            throw new SystemException("Location not assigned a domain");
        ThreadProperties.getSession().update(location);
    }

    public void saveLocation(final Location location) {
        if (location.getDomain() == null)
            throw new SystemException("Location not assigned a domain");
        ThreadProperties.getSession().save(location);
    }

    public Locale findLocale(final String code) {
        Locale locale = null;
        Session session = ThreadProperties.getSession();
        locale = (Locale) session.load(Locale.class, code);
        return locale;
    }

    public void updateLocation(final Long id, final Domain domain, final Long parentId, final String name,
            final String description) {
        Location location = findLocation(id);
        if (location == null) {
            log.error("updateLocation() could not find id = " + id);
            throw new DataException("upDateLocation() could not find location id = " + id);
        }
        Session session = ThreadProperties.getSession();
        location.setId(id);
        location.setDescription(description);
        location.setName(name);
        location.setDomain(domain);
        session.update(location);
    }

    public void removeLocation(final Person person, final Long locationId) {
        log.debug("removeLocation() - called");
        Location location = findLocation(locationId);
        if (location != null) {
            Session session = ThreadProperties.getSession();

            log.debug("removeLocation() - found location, deleting...");
            location.setVoided(true);
            session.saveOrUpdate(location);

            throw new SystemException("Remove should not be used for location until UI management is complete");
        } else
            throw new DataException("Could not find location in order to delete.  Location_id= " + locationId);
    }

    public List findTopLevelLocations(final Domain domain) {
        log.debug("findTopLevelLocations() - called");
        Session session = ThreadProperties.getSession();
        Filter filter = new Filter();
        filter.addFrom("org.vectrics.location.Location", "location");
        filter.addSelectField("distinct(location)");
        filter.addOrderBy("location.name");
        filter.addOrderBy("location.id");
        filter.addCondition("location.parent.id is null");

        Selector domainSelector = filter.getSelector().addChildSelector(Selector.CONJUNTION_OR);
        Domain currentDomain = domain;
        while (currentDomain != null) {
            domainSelector.addParameterCondition("location.domain.id", "=", currentDomain.getId());
            currentDomain = currentDomain.getParent();
        }

        log.debug("findAllLocations() - running query...");
        List all = filter.createQuery(session).list();
        log.debug("findAllLocations() - closing session");
        return all;

    }

    public List<Location> findTopLevelLocations(final Domain domain, final String locationType) {
        TreeSet<Location> result = new TreeSet<Location>(new LocationComparator());
        Session session = ThreadProperties.getSession();
        Filter filter = new Filter();
        filter.addFrom("org.vectrics.location.Location", "location");
        filter.addFrom("org.vectrics.location.DomainLocation", "doLocation");
        filter.addOrderBy("location.name");
        filter.addOrderBy("location.id");
        filter.addSelectField("location");

        filter.addCondition("location.id=doLocation.location.id");
        filter.addCondition("location.parent.id is null");
        filter.addParameterCondition("doLocation.domain.id", "=", domain.getId());
        filter.addParameterCondition("doLocation.typeCode", "=", locationType);
        log.debug("findAllLocations() - running query...");
        List<Location> all = filter.createQuery(session).list();
        log.debug("findAllLocations() - closing session");
        return all;
    }

    public List<Location> findAllLocations(final Domain domain) {
        log.debug("findAllLocations() - called");
        Session session = ThreadProperties.getSession();
        log.debug("findAllLocations() - running query...");

        Filter filter = new Filter();
        filter.addFrom("org.vectrics.location.Location", "location");
        filter.addSelectField("location");

        Selector domainSelector = filter.getSelector().addChildSelector(Selector.CONJUNTION_OR);
        Domain currentDomain = domain;
        while (currentDomain != null) {
            domainSelector.addParameterCondition("location.domain.id", "=", currentDomain.getId());
            currentDomain = currentDomain.getParent();
        }
        log.debug("findAllLocations() - adding all locations to sorted list...");
        List<Location> locationList = filter.createQuery(session).list();
        locationList.addAll(locationList);
        log.debug("findAllLocations() - closing session");
        return locationList;
    }

    public Location findLocationForAddress(final Address address) {
        log.debug("findLocationForAddress()");
        Location location = null;
        /*
         * Collection locations = null; Session session = null; try { session =
         * hibernate.openSession(); Collection paramValues = new ArrayList();
         * paramValues.add(address.getCountryCode());
         * paramValues.add(address.getPostalCode());
         * 
         * Collection paramTypes = new ArrayList();
         * paramTypes.add(Hibernate.STRING); paramTypes.add(Hibernate.STRING);
         * Type[] types = null; locations = session.find(
         * "postalCodeLocation.location from PostalCodeLocation postalCodeLocation"
         * +
         * " where (postalCodeLocation.country.code = ?) and (postalCodeLocation.postalCode = ?) and (postalCodeLocation.voided = 0)"
         * , paramValues.toArray(), types); if (locations != null) { Iterator
         * iter = locations.iterator(); if (iter.hasNext()) { location =
         * (Location)iter.next(); } } } catch (Exception he) { throw new
         * DataException(he); } finally { hibernate.closeSession(session); }
         */
        return location;
    }

    public Address findAddress(final Long addressId) {
        log.debug("findAddress() - called");
        Address address = null;
        Session session = ThreadProperties.getSession();
        address = findAddress(session, addressId);
        return address;
    }

    public Address findAddress(final Session session, final Long addressId) {
        log.debug("findAddress(session, ...) - called");
        Address address = null;
        address = (Address) session.load(Address.class, addressId);
        return address;
    }

    public Address createAddress() {
        Address address = new Address();
        address.setAddress1("");
        address.setLocation(null);
        address.setPostalCode("");
        log.debug("createAddress() - saving work address ...");
        Session session = ThreadProperties.getSession();
        session.save(address);
        return address;
    }

    public void updateAddress(final Address updateAddress) {
        if (updateAddress == null)
            throw new DataException("updateAddress() had null address passed to it");
        Session session = ThreadProperties.getSession();
        session.saveOrUpdate(updateAddress);
    }

    public void removeAddress(final Address address) {
        address.setVoided(true);
        ThreadProperties.getSession().update(address);
    }

    public List<Location> findChildren(final Location location) {
        List<Location> children = null;
        Session session = ThreadProperties.getSession();
        children = session
                .createQuery("from Location location where (location.parent.id = ?) and (location.voided = 0)")
                .setLong(0, location.getId().longValue()).list();
        return children;
    }

    private void addLocationAndChildrenToSelector(final Location parent, final Selector idSelector) {
        idSelector.addParameterCondition("location.id", "=", parent.getId());
        Collection children = findChildren(parent);
        Iterator iter = children.iterator();
        while (iter.hasNext()) {
            Location child = (Location) iter.next();
            addLocationAndChildrenToSelector(child, idSelector);
        }
    }

    public Filter createLocationFilterFromCriteria(final LocationCriteria criteria) {
        log.debug("getFilterForCriteria()");
        Filter filter = new Filter();
        filter.setConjunction(Selector.CONJUNTION_AND);
        filter.addFrom("Location", "location");
        filter.addSelectField("location");
        filter.addOrderBy("location.name");

        if (StringUtils.isNullOrBlank(criteria.getName()) == false) {
            filter.addCondition("location.name like '" + criteria.getName() + "%'");
        }
        log.debug("Add domains to query along with parents");
        Iterator domainsIter = criteria.getDomains().iterator();
        while (domainsIter.hasNext()) {
            Domain domain = (Domain) domainsIter.next();

            Selector domainSelector = filter.getSelector().addChildSelector(Selector.CONJUNTION_OR);
            Domain currentDomain = domain;
            while (currentDomain != null) {
                domainSelector.addParameterCondition("location.domain.id", "=", currentDomain.getId());
                currentDomain = currentDomain.getParent();
            }
        }

        if (criteria.getParentList() != null && criteria.getParentList().size() > 0) {
            log.debug("Adding companies to query");
            Iterator iter = criteria.getParentList().iterator();
            if (iter.hasNext()) {
                Selector idSelector = filter.getSelector().addChildSelector(Selector.CONJUNTION_OR);
                while (iter.hasNext()) {
                    Location parent = (Location) iter.next();
                    addLocationAndChildrenToSelector(parent, idSelector);
                }
            }
        }

        if (criteria.getOnlyChildless()) {
            // TODO need some sql that excludes locations where other locations
            // exist with
            // it as a parent
        }

        if (criteria.getLocationType() != null && !criteria.getLocationType().trim().equals("")) {
            filter.addFrom("DomainLocation", "domainLocation");
            filter.addCondition("location.id = domainLocation.location.id");
            filter.addParameterCondition("domainLocation.typeCode", "=", criteria.getLocationType());
        }

        if (criteria.getVoidedType() == Criteria.VOID_NO) {
            filter.addParameterCondition("location.voided", "=", "F");
        }

        return filter;
    }

    public Collection findLocations(final LocationCriteria criteria, final int startIndex, final int endIndex) {
        Filter filter = createLocationFilterFromCriteria(criteria);
        Collection locations = HibernateUtil.findElementsFromFilter(filter, startIndex, endIndex);
        return locations;
    }

    public boolean hasDomainLocation(final Domain domain, final Location location, final String contextCode) {
        DomainLocation domainLocation = findDomainLocation(domain, location, contextCode);
        if (domainLocation == null)
            return false;
        return true;
    }

    /**
     * Find the domain location object for the passed domain, location,
     * contextCode.
     */
    public DomainLocation findDomainLocation(final Domain domain, final Location location, final String contextCode) {
        Filter filter = new Filter();
        filter.addFrom("DomainLocation", "domainLocation");
        filter.addSelectField("domainLocation");
        filter.addParameterCondition("domainLocation.domain.id", "=", domain.getId());
        filter.addParameterCondition("domainLocation.location.id", "=", location.getId());
        filter.addParameterCondition("domainLocation.typeCode", "=", contextCode);

        Iterator iter = filter.createQuery(ThreadProperties.getSession()).iterate();
        DomainLocation domainLocation = null;
        if (iter.hasNext()) {
            domainLocation = (DomainLocation) iter.next();
        }

        return domainLocation;
    }

    /**
     * Find all locations for a specific context.
     */
    public List findLocationsForContextType(final Domain domain, final String contextCode) {
        Filter filter = new Filter();
        filter.addFrom("DomainLocation", "domainLocation");
        filter.addSelectField("domainLocation.location");
        filter.addParameterCondition("domainLocation.domain.id", "=", domain.getId());
        filter.addParameterCondition("domainLocation.typeCode", "=", contextCode);

        filter.addOrderBy("domainLocation.location.name");
        filter.addOrderBy("domainLocation.location.id");

        List list = filter.createQuery(ThreadProperties.getSession()).list();

        return list;
    }

    /**
     * @see org.vectrics.location.LocationService#deleteLocation(Location)
     */
    public void deleteLocation(final Location location) {
        Session session = ThreadProperties.getSession();
        session.delete(location);
    }

    /**
     * @see org.vectrics.location.LocationService#findLocationForCode(String,
     *      String)
     */
    public Collection findLocationForCode(final String name, final String typeCode) {
        Session session = ThreadProperties.getSession();
        Filter filter = new Filter();
        filter.addFrom("org.vectrics.location.Location", "location");
        filter.addSelectField("distinct(location)");
        filter.addParameterCondition("location.name", "like", name);
        filter.addParameterCondition("location.typeCode", "=", typeCode);

        List all = filter.createQuery(session).list();

        return all;
    }

    /*
     * (non-Javadoc)
     * 
     * @see
     * org.vectrics.location.LocationService#findAllLocations(org.vectrics.domain
     * .Domain, java.lang.String)
     */
    @Override
    public List<Location> findAllLocations(final Domain domain, final String locationType) {
        log.debug("findAllLocations() - called");
        Session session = ThreadProperties.getSession();
        log.debug("findAllLocations() - running query...");

        Filter filter = new Filter();
        filter.addFrom("org.vectrics.location.Location", "location");
        filter.addFrom("org.vectrics.location.DomainLocation", "doLocation");
        filter.addOrderBy("location.name");
        filter.addOrderBy("location.id");
        filter.addSelectField("location");

        filter.addCondition("location.id=doLocation.location.id");
        filter.addParameterCondition("doLocation.domain.id", "=", domain.getId());
        filter.addParameterCondition("doLocation.typeCode", "=", locationType);
        // Selector domainSelector =
        // filter.getSelector().addChildSelector(Selector.CONJUNTION_OR);
        // Domain currentDomain = domain;
        // while (currentDomain != null) {
        // domainSelector.addParameterCondition("location.domain.id", "=",
        // currentDomain.getId());
        // currentDomain = currentDomain.getParent();
        // }
        log.debug("findAllLocations() - adding all locations with locationType to sorted list...");
        List<Location> locationList = filter.createQuery(session).list();
        log.debug("findAllLocations() - closing session");
        return locationList;
    }

}
